﻿namespace Okazuki.MVVM.PrismSupport.ViewModels
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Reflection;
    using Microsoft.Practices.Prism.ViewModel;

    /// <summary>
    /// IDataErrorInfoとDataAnnotationsによるバリデーションを行うViewModel
    /// </summary>
#if SILVERLIGHT
    public abstract class ValidatableViewModelBase : ViewModelBase, IDataErrorInfo, INotifyDataErrorInfo
#else
    public abstract class ValidatableViewModelBase : ViewModelBase, IDataErrorInfo
#endif
    {
        // プロパティのキャッシュ
        private Dictionary<string, PropertyInfo> propertyCache;

        private ErrorsContainer<ValidationErrorViewModel> errors;

        private object syncObject = new object();

        /// <summary>
        /// このオブジェクトのバリデーションエラー情報を管理するオブジェクトを取得します。
        /// </summary>
        protected ErrorsContainer<ValidationErrorViewModel> Errors
        {
            get
            {
                if (this.errors == null)
                {
                    lock (syncObject)
                    {
                        if (this.errors == null)
                        {
                            this.errors = new ErrorsContainer<ValidationErrorViewModel>(p =>
                            {
                                this.RaisePropertyChanged("HasErrors");
                                this.RaisePropertyChanged("ValidationErrors");
#if SILVERLIGHT
                                this.OnErrorChanged(p);
#endif
                            });
                        }
                    }
                }

                return this.errors;
            }
        }

        private Dictionary<string, PropertyInfo> PropertyCache
        {
            get
            {
                if (this.propertyCache == null)
                {
                    propertyCache = this.GetType()
                        .GetProperties()
                        .Where(p => p.GetIndexParameters().Length == 0)
                        .ToDictionary(p => p.Name);
                }
                return this.propertyCache;
            }
        }

        string IDataErrorInfo.Error
        {
            get { throw new NotSupportedException(); }
        }

        /// <summary>
        /// 指定したプロパティのエラーを返す
        /// </summary>
        /// <param name="columnName">プロパティ名</param>
        /// <returns>エラー情報</returns>
        public string this[string columnName]
        {
            get
            {
                return this.Errors
                    .GetErrors(columnName)
                    .Select(e => e.ErrorMessage)
                    .FirstOrDefault();
            }
        }

        /// <summary>
        /// エラーがある場合はtrue
        /// </summary>
        public bool HasErrors
        {
            get
            {
                return this.Errors.HasErrors;
            }
        }

        /// <summary>
        /// 現在のバリデーションエラー情報
        /// </summary>
        public IEnumerable<ValidationErrorViewModel> ValidationErrors
        {
            get
            {
                return this.PropertyCache
                    .Keys
                    .Select(p => this.Errors.GetErrors(p))
                    .SelectMany(p => p);
            }
        }

        /// <summary>
        /// プロパティの妥当性検証とPropertyChangedイベントを発行します。
        /// </summary>
        /// <param name="propertyName">プロパティ名</param>
        protected override void RaisePropertyChanged(string propertyName)
        {
            this.ValidateProperty(propertyName);
            base.RaisePropertyChanged(propertyName);
        }

        /// <summary>
        /// 全てのプロパティの妥当性検証を行います
        /// </summary>
        public void ValidateProperties()
        {
            foreach (var propertyName in this.PropertyCache.Keys)
            {
                this.ValidateProperty(propertyName);
            }
        }

        /// <summary>
        /// 指定したプロパティの妥当性検証を行います。
        /// 通常は、プロパティの値の変更と同時に検証が行われるため通常は呼び出す必要はありません。
        /// </summary>
        /// <param name="propertyName">プロパティ名</param>
        public void ValidateProperty(string propertyName)
        {
            var value = this.PropertyCache[propertyName].GetValue(this, null);
            List<ValidationResult> results = new List<ValidationResult>();

            if (Validator.TryValidateProperty(
               value,
               new ValidationContext(this, null, null)
               {
                   MemberName = propertyName
               },
               results))
            {
                this.Errors.ClearErrors(propertyName);
            }
            else
            {
                this.Errors.SetErrors(propertyName, 
                    results.Select(
                        r => new ValidationErrorViewModel(
                            r.MemberNames.First(), 
                            r.ErrorMessage)));
            }
        }
        
#if SILVERLIGHT
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
        protected virtual void OnErrorChanged(string propertyName)
        {
            var h = this.ErrorsChanged;
            if (h != null)
            {
                h(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        public System.Collections.IEnumerable GetErrors(string propertyName)
        {
            return this.Errors.GetErrors(propertyName);
        }
#endif
    }
}
